In Oberon/F, several levels of programming can be distinguished.
The first and simplest level is the programming of commands. A command is a procedure that performs one or several operations like opening a window, pasting a text, stretching or selecting parts of a graph. Commands are usually invoked interactively by the user, e.g. by selecting a menu entry or by clicking on a button, and they typically operate on abstract data types, e.g. on texts or on forms.
The second level of programming is the programming of views. A view provides a visual presentation of data, e.g. in a window or as part of another view. Views can be stored and printed, and their contents can be edited interactively, e.g. through cut, copy, drag and drop, etc.
The third and most involved level of programming is the programming of containers. A container is a view which can contain other views, e.g. a graphics view may contain captions which actually are normal text views. A text view is another container example since it may contain other views, e.g. a graphics view, and so on.
In this tutorial, there is one chapter for the first and one chapter for the second programming level. Container programming is not described here.
This tutorial introduces the most fundamental aspects of Oberon/F by means of graduated examples. It doesn't attempt to show all features supported by Oberon/F. Your copy of the software may contain additional documentation on-line or as hard-copy, e.g. commented sources of container views or examples highlighting more specialized aspects of the framework. For a complete and systematic presentation of the Oberon/F interfaces, the reader is referred to the reference part of this book.
Command Programming
This chapter presents several simple example programs, which demonstrate how the standard text and form abstractions can be used from within a program.
3.1 Working with Texts
When using a new programming environment, many programmers first write a program which puts the string "Hello World" onto the screen. In this tutorial, you'll find several such "Hello World" programs with different degrees of sophistication.
Our first "Hello World" program writes the string "Hello World" into the log window. This window is usually open when Oberon/F is used as programming environment (whether or not the log window is opened on startup of Oberon/F depends on the configuration, which the user can change to his liking, as described in the Oberon/F User's Guide.
MODULE ObxEx0;
IMPORT Out;
PROCEDURE Do*;
BEGIN
Out.String("Hello World"); Out.Ln
END Do;
END ObxEx0.
In this first example program, we can see several properties of an Oberon/L program:
Such an Oberon program consists of one or more modules, e.g. ObxEx0.
One module may use ("import") other modules, e.g. ObxEx0 imports Out.
Standard modules of Oberon can be imported by new modules, e.g. ObxEx0 imports the library module Out.
Procedures of another module are referenced as "module.procedure", e.g. Out.String or Out.Ln.
Procedures which are accessible to other modules must be marked as "exported" (the "*" mark of ObxEx0.Do).
The behavior of a program is provided to the system simply as one or several exported procedures, e.g. ObxEx0.Do.
In order to toy around with the above example program, you can open a text window (actually a window containing a document containing a text view) by choosing New in the File menu. After you have typed in the above source text, you can compile the module by choosing Compile in the Dev menu. In the log window you can see whether compilation was successful.
If no error message has appeared in the log, you can execute procedure ObxEx0.Do by selecting the string "ObxEx0.Do" (just type it in after the module's text, in the same window) and then choosing Execute in the Dev menu. As a result, "Hello World" is written into the log. You can repeat Execute as often as you like.
In Oberon/L, an exported procedure like ObxEx0.Do is called a command, if it should be called from the user interface, rather than from other modules (although this is also possible). Commands can be activated in various ways. In production software, commands are typically activated by selecting a menu entry or by clicking on a button.
Since module Out is a useful tool, we want to give an overview over its facilities, so we can use it in later example programs. An overview over a module's facilities is called the module's definition. Usually, a module definition in Oberon is written like the module itself, but with all non-exported items left out, with the procedure bodies eliminated, without the export marks, and with the pseudo-keyword DEFINITION instead of MODULE. Note however, that this is merely a convention for documentation purposes, and that the compiler cannot use a definition as input. The Browser automatically generates a definition text out of a compiled module's symbol file (see
User's
Guide
Here is the definition of module ObxEx0:
DEFINITION ObxEx0;
PROCEDURE Do;
END ObxEx0.
The definition of module Out is given below.
DEFINITION Out;
PROCEDURE Open;
PROCEDURE Char (ch: CHAR);
PROCEDURE Ln;
PROCEDURE Int (i, n: LONGINT);
PROCEDURE Real (x: REAL; n: INTEGER);
PROCEDURE String (str: ARRAY OF CHAR);
END Out.
Open opens a log window, if there isn't one open already. In our examples here, we assume the log to be open, and thus never call this procedure. The other procedures write characters, carriage returns, integers, reals, and strings into the log. The parameter n in the procedures Int and Real denotes the number of digits to be used in the number's representation. If it is smaller than necessary (e.g. zero), the minimal number of digits is used.
Another example of the use of module Out is the following program, whose ObxEx1.Sum command calculates the sum 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7:
MODULE ObxEx1;
IMPORT Out;
PROCEDURE Sum*;
VAR i, sum: INTEGER;
BEGIN
i := 1; sum := 0;
WHILE i <= 7 DO
sum := sum + i;
i := i + 1
END;
Out.String("the sum of 0..7 yields "); Out.Int(sum, 0); Out.Ln
END Sum;
END ObxEx1.
Alas, this program and the first "Hello World" mix their outputs in one and the same window, the log. Thus we'll present another "Hello World" program which explicitly generates a text, inserts this text in a text view, and opens this text view in its own window; independent of the log window. For the time being, a "view" may simply be regarded as the contents of a window.
A text is an abstract data type, i.e. a data type which hides its internal workings in the module in which it is defined (i.e. in module TextModels). A text contains a sequence of characters, plus character attributes like font or color. A text does not know how to display itself, this capability is embodied in a text view. A text view is also an abstract data type, defined in module TextViews.
Our next program imports TextModels and TextViews, plus modules Views. Furthermore, module TextMappers is imported, which provides services to generate or to scan formatted text:
MODULE ObxEx2;
IMPORT Views, TextModels, TextMappers, TextViews;
PROCEDURE Do*;
VAR t: TextModels.Model; f: TextMappers.Formatter; v: TextViews.View;
BEGIN
t := TextModels.dir.New(); (* produce an empty text *)
f.ConnectTo(t); (* open formatter on text *)
f.WriteString("Hello World"); f.WriteLn; (* write string and line separator *)
v := TextViews.dir.New(t); (* produce a view which displays text t *)
Views.OpenView(v) (* open the view in a window *)
END Do;
END ObxEx2.
For this example, we can make the following remarks:
Texts and text views are produced by so-called directory objects, namely TextModels.dir and TextViews.dir.
Directory objects are exported global variables which contain New functions.
Directory objects allocate and initialize variables of abstract data types.
TextMappers provides a data type called a Formatter, which can write formatted output into a text object.
The text formatter provides formatting procedures like WriteString or WriteLn.
The text formatter is connected to the text t.
The view is opened in a window, whose contents can be edited, stored, or printed; just like any other window that contains a text.
Generating text output is often useful, but seldom sufficient. Usually we need a way to pass one or several input values to a program, and often a text is a suitable input type (e.g. for the compiler).
In Oberon/F, visible text is represented in a text data structure, and this data structure can be accessed and used as input by any command. This makes it possible to use text produced by one command as input for another command.
For example, the Loaded
Modules command in the Info menu generates a list of all loaded modules. This list is simply a text; it can be edited, stored, and printed just like any text typed in by the user.
The menu item Commands in the same menu takes such a text, and looks for a text selection. If it finds a selection, it reads the text in the selection (input!) and interprets it as the name of a module. If it finds a module with this name, it opens a window with all exported parameterless procedures of this module. This is an example of how a command uses a text generated earlier as input, in order to produce a new text as output.
We now want to take a closer look at how a command like Commands gets access to the text produced by Loaded
Modules. The following program shows what happens in essence:
MODULE ObxEx3;
IMPORT TextControllers, Out;
PROCEDURE Do*;
VAR c: TextControllers.Controller; beg, end: LONGINT;
BEGIN
c := TextControllers.Focus(); (* get the focus controller, if any *)
IF (c # NIL) & c.HasSelection() THEN (* a text view with a selection must be focus*)
c.GetSelection(beg, end);
Out.String("beg = "); Out.Int(beg, 0); Out.Ln;
Out.String("end = "); Out.Int(end, 0); Out.Ln
ELSE
Out.String("no text focus or no selection"); Out.Ln
END
END Do;
END ObxEx3.
Whenever a command needs the contents of a window as input, it must be able to get at the appropriate window, and from there to its contents. At any time, Oberon/F provides access to one window's contents, this window is called the focus window. In user interfaces which support overlapping windows, the focus window is usually the front window.
Sometimes, the role of focus window is actually split between two windows: the front window and a target window. In this case, the target window is a document (e.g. a text document), and the front window a dialog. In the normal case however, the front window also acts as target window.
Menu commands operate on the front window, while commands invoked through buttons in the front window operate on the target window.
Picture a: Window which is both Front and Target Window
Picture b :Target Window is still the same, but a Dialog has become Front Window
A command implementation can get at its appropriate focus by calling a suitable function, e.g. TextControllers.Focus as in the above example. This shows that there is another important module in the text subsystem: TextControllers. A text controller handles the interaction of the user with a text displayed in a text view.
After having checked that the focus controller is not NIL and that there currently is a selection, ObxEx3 writes the beginning and end positions of the selected text to the log. A text position denotes the left edge of a character, starting at position 0:
Picture c Character Positions
Now we want to write the contents of the selection to the log:
MODULE ObxEx4;
IMPORT TextModels, TextControllers, Out;
PROCEDURE Do*;
VAR c: TextControllers.Controller; beg, end: LONGINT;
r: TextModels.Reader; ch: CHAR;
BEGIN
c := TextControllers.Focus(); (* get the focus controller, if any *)
IF (c # NIL) & c.HasSelection() THEN (* a text view with a selection must be focus*)
c.GetSelection(beg, end);
r := c.text.NewReader(NIL); (* create a reader for the view's text *)
r.SetPos(beg); (* position reader at beginning of selection *)
r.ReadChar(ch); (* read first character of selection *)
WHILE (r.Pos() <= end) & ~r.eot DO (* stop at end of selection and at end of text *)
Out.Char(ch);
r.ReadChar(ch) (* read next character in selection *)
END;
Out.Ln
ELSE
Out.String("no text focus or no selection"); Out.Ln
END
END Do;
END ObxEx4.
This program is the same as ObxEx3, except for the parts in bold face, which are either new or changed.
This example shows the use of a TextModels.Reader. A text reader is an object which implements an access path to a text. The text itself creates its readers. A reader has a position on its text, and provides procedures for positioning on the text (SetPos), for inquiring the reader's current position (Pos) and for reading characters (ReadChar). After every character read, the reader's position is incremented by one.
3.2 Working with Forms
Forms are essential business tools. Electronic forms are documents which can be manipulated in two special modes: in one mode, a form's layout is edited (layout mode). The graphical elements that appear in a form layout are sometimes called controls. Typically, most of the graphical elements are text fields, followed by a variety of buttons. In layout mode, controls are passive: buttons can be selected, but not "pressed". No control can be made focus.
In the other mode (mask mode), a form is used for data entry, usually in a dialog window. In this mode, the form's layout cannot be changed anymore, but the controls become active, i.e. text can be entered in text fields, buttons can be pressed, etc. In mask mode, no control may be selected.
(In fact, it is also possible to enable both focus and selection, or to disable both, giving rise to an editor mode and a viewer mode, respectively. For forms processing, these additional modes are not relevant, however.)
In typical electronic business forms, a button that is depressed causes an action on the form's contents, e.g. the data entered into the form is stored in a data base and cleared thereafter. However, there is an important special case of electronic forms where a button's action does not operate on the contents of its own form, but on another document' contents, in another window. The data that such a form contains act as parameters to its commands (i.e. its buttons). For example, a "Find & Replace" dialog may contain a text field labeled "find" and a button labeled "Find Next". The text field contains the string to be searched, and the button activates the search command. The search command then operates not on the dialog contents (the front window), but on the contents of the window beneath the dialog (the target window).
A dialog which acts as front but not simultaneously as target window is called a tool. A dialog which acts both as front and as target window is shown in an auxiliary window. Auxiliary windows can be recognized by the [ and ] brackets around their titles, e.g. "[ New Form ]". On some platforms, tool windows are denoted by angular brackets around their titles, e.g. "<<
Replace
In contrast to most other development systems on the market today, Oberon/F treats dialogs and controls the same way as it does normal document components, they are just views that form part of a compound document. Consequently, dialogs are stored in the same file format as are all other Oberon/F documents. The result of such a unification of compound documents and user interface controls is sometimes called a compound user interface.
Oberon/F currently only supports non-modal forms, i.e. dialogs or data entry masks which don't block the user from doing something else while the form is open, e.g. looking up information about the task currently performed. This means that windows with forms can be put away anytime, to suspend the task for a while and to later resume it, by bringing the window to the top again.
The following examples will demonstrate how a command may acquire its parameters through a dialog, and how variables and commands of a program can be linked to a form's controls.
Ideally, forms design should be done by a professional in the graphics design and human interface fields, and program design should be done by a professional in the software engineering field. Thus these distinct tasks should be dependent on each other as little as possible. Yet the software engineer's final program must be able to work with the various forms that the human interface designer has developed, so some dependencies obviously exist.
These dependencies consist of three different parts: a program must manipulate data contained in a form, thus it must be able to get this data into its program variables, it must be able to put computed results residing in its program variables back into the form, and one of its procedures must be triggered when the user performs an interaction with a control.
Oberon/F handles these dependencies in a very convenient way: The main idea is that the programmer should be relieved from mundane tasks like transferring data between program variables and controls as much as possible. Rather, a control should be able to monitor a program variable directly. Thus, when the user edits the data in a control, the change is immediately reflected in the corresponding program variable, without the need for intervention by the programmer. The programmer combines all variables to be presented in a form in one Oberon/L record, called an interactor. Thus the record corresponds to the form, and the record fields to individual controls. A benefit of this convention is that Oberon/F can automatically generate a form out of a suitably defined record declaration. For example, an integer field is by default translated into a text entry field which only accepts integer numbers, a procedure-typed record field is translated into a push button, etc. The automatic generation of a form is particularly convenient if a user interface is only temporary, e.g. for testing purposes. In essence, it is a rapid prototyping aid.
Forms may also be defined first, and the suitable program be written latter. It is also possible to later change the linking between controls and program variables, to change the type of a variable's control (e.g. a text entry field for numbers may be replaced by a scroll bar), to edit the form's layout whenever desired, or to add graphical elements which do not appear as variables in the program because their use is only for decoration (e.g. a company logo in a form).
Remember that a form is stored in the standard Oberon/F compound document file format, and thus remains editable at any time. This is convenient e.g. when forms need to be translated from one (natural) language into another one, e.g. from English to German. In Oberon/F this is possible for someone who has no access to the program's source code, because no recompilation is necessary. This is in contrast to most other development systems which translate a form specification - even if generated using an interactive editor - into a program source text, which must be compiled and linked to the rest of the program. Such a source-code-generator approach can be considered obsolete today.
The consistency of an Oberon/F program with its forms is checked whenever a form is loaded. A control which is inconsistent with the variable to which it is linked (e.g. because the variable has been removed) can still be displayed, but is made inactive. This load-time check is possible because Oberon/F contains an advanced meta-programming system which makes all necessary type information available at run-time.
The following example module exports a global variable, for which a dialog can be generated with the New
- A variable find is declared as global record-typed global variable.
- For every procedure-typed record field there is a parameterless global procedure
- In the module's initialization part the procedure-typed fields of variable find are assigned
Picture a : "New Form" Dialog
You can try out this example in the following way: invoke New
Form... in the Dev menu, which opens the standard form generation dialog. In this dialog, type in "ObxEx5.find" in the link field, then click on the command button. As a result, a dialog is generated and opened. Click on the various buttons, and look at the output in the log window.
Obviously, ObxEx5 does not actually implement a find & replace algorithm, but merely writes into the log. However, this is sufficient as a demonstration.
A newly generated form is opened in layout mode, i.e. the form elements can be moved around, resized, and their properties may be modified using a special inspector tool. After editing, the form can be saved, and opened as a tool, where it can be used. To do that, execute Open
Dialog in the Dev menu.
In this way, you get a window in layout mode simultaneously with another one in mask mode, for the same form, as in the example below:
Picture c : Layout and Mask for the "Find & Replace" Dialog
Our next example program is a variation of ObxEx1. In contrast to that older version, it allows to enter the upper limit of the sum to be calculated, using a form:
MODULE ObxEx6;
IMPORT Dialog, Out;
param*: RECORD (Dialog.Interactor)
limit*: LONGINT;
Sum*: PROCEDURE
END;
PROCEDURE Sum*;
VAR i, sum: INTEGER;
BEGIN
i := 1; sum := 0;
WHILE i <= param.limit DO
sum := sum + i;
i := i + 1
END;
Out.String("the sum of 0.."); Out.Int(param.limit, 0); Out.String(" yields ");
Out.Int(sum, 0); Out.Ln;
param.limit := 0; Dialog.Update(param)
END Sum;
BEGIN
param.Sum := Sum
END ObxEx6.
After a form has been generated for ObxEx6.param, the user can enter a number in the limit control. When the user clicks the Sum button, the sum of 0 .. ObxEx6.param.limit is calculated and written to the log. Afterwards, the ObxEx6.param.limit field is set to 0. While an interactive modification of a control is automatically reflected in the program variable, the opposite is not true: after assigning a new value to the ObxEx6.param.limit field, Oberon/F must be notified of the change. This is done with the Dialog.Update procedure. In order to be able to use this procedure, the interactor must be defined as an extension of a Dialog.Interactor.
3.3 Working with Commands
In Oberon/L, an exported procedure to be called by the user is called a command. In any Oberon environment, the user should be able to call a command, given the command's name and the name of the module that contains the command. A command is the atomic action that a user can execute, which means that commands may only be executed sequentially, not concurrently.
A command always performs two tasks: first, it assembles the parameters that it needs. These parameters can be any part of the whole Oberon environment's global state. If a command uses global state about which the user does not know, the command becomes difficult to use, since its effect becomes unpredictable from a user's point of view. Such hidden modes should be avoided in good user interface designs, thus commands should only depend on state which is somehow visible to the user, e.g. the focus, its selection or caret, etc.
After the command's parameters have been gathered, an action is invoked with these parameters, e.g. closing a window, deleting selected text, or storing a form's contents into a database.
For the user, there may be several ways to call a command. One way might be to press a button in a form, another way may be the activation of a text stretch which has the form "module.procedure". However, the most typical way is to select a menu item.
After the core of Oberon/F has been started up, it tries to find a text document with file name Menus. This text contains the initial menu configuration. An example may look as follows:
menuname, typename, itemname, shortcut, command, and guard are strings delimited by double quotes.
typename must contain the name of a record type in the form "module.record". Commands and guards must contain Oberon/L command sequences.
A menu consists of several items. An item is either a single menu entry, or a separator. Separators are used to visually group menu entries in a menu. A menu entry has a visible representation (the item's name, as a string), possibly a keyboard shortcut, a command, and a guard. When the menu entry is selected, the command described by the command string is executed.
By editing the Menus text, menu items or whole menus can be deleted or added.
Mac OS:
The standard menus like File, Edit, Attributes, and Windows cannot be reconfigured however, and thus do not appear in the configuration text.
Note that several menus of the standard configuration have a type name after the keyword MENU, written in parentheses. If no such type name is given, the menu is always available. If a type name is given, the corresponding menu is only available if the focus is of the given type. The text menu, for example, only appears when a text view is focused.
Menus can be changed at run-time: create the menu text with the Menus command in the Info menu. Then add for example the following item to the Dev menu:
Menus command in the Info menu to update the menus. Now you can execute the new command by selecting the appropriate menu entry. It attempts to paste a clock into the front window's document.
If you want to make the new menu configuration permanent, save the menu text in the Rsrc subdirectory, which resides in the System subdirectory of Oberon/F.
3.4 Working with Files
In Oberon/F, files play a less central role than in most programming environments, because the programmer only works with files indirectly. You stand a good chance that you'll never use the file interface directly. For this reason, we don't recommend that you concentrate on files when learning about Oberon/F, you should rather look at Oberon/F views (see next chapter) as the pivotal abstraction of the component framework. The only reasons for using files directly are the handling of legacy files produced with other software, and the implementation of simple file-based database systems.
However, since files are important traditional abstractions in computer science, and form the basis of the persistent storage mechanism of Oberon/F, a short introduction is given anyway.
Normally, data is written to a file ("externalized") and read from a file ("internalized") via so-called mapper objects, which perform the mapping of byte streams to higher-level data structures and vice versa. These mappers are provided by the framework where they are needed. Module Stores provides two such mappers, a Stores.Reader for internalization and a Stores.Writer for externalization. File mappers internally contain objects which connect them to files. These connecting objects are called file riders. There are two flavors of file riders, one for internalization and one for externalization.
Picture a File with three Riders
We'll first show how files can be accessed through direct use of their riders, and then how they can be used via file mappers. The first of our examples demonstrates how an existing file is looked up, how its contents are copied to a new file, and how the new file is permanently registered in the file directory:
MODULE ObxEx7;
IMPORT Files;
PROCEDURE Do*;
VAR s: Files.Locator; old, new: Files.File; r: Files.Reader; w: Files.Writer;
ch: CHAR; res: LONGINT;
BEGIN
s := Files.dir.This("Obx"); (* get locator for Obx subdirectory *)
s := s.This("Samples"); (* get locator for Samples subdirectory *)
old := Files.dir.Old(s, "Test0", Files.shared); (* look up file "Test0" *)
ASSERT(old # NIL, 20); (* abort program if file not found *)
new := Files.dir.New(s); (* create a new file at same location*)
r := old.NewReader(NIL); r.SetPos(0); (* create a rider for internalization *)
w := new.NewWriter(NIL); w.SetPos(0); (* create a rider for externalization *)
r.ReadByte(ch);
WHILE ~r.eof DO (* stop when end of file is reached *)
w.WriteByte(ch);
r.ReadByte(ch)
END;
new.Register("Test1", old.type, res) (* register copy as "Test1" *)
END Do;
END ObxEx7.
In this program, a file is looked up in three steps. In the first step, the file directory object Files.dir delivers a locator for subdirectory Obx. All other file directory operations take such a locator as parameter. In the second step, a locator for directory Samples in Obx is created, i.e. of directory Obx/Samples. In the third step, the directory delivers a file object, provided a file with the given name exists. Files may be opened in shared mode or in exclusive mode. Usually, the shared mode is used, which means that several invocations of Files.dir.Old for the same file yield the same file object, but that no one may alter the contents of this file.
If the file's contents should be altered, a new file is generated instead. After the new file has been written, it replaces the old one. This replacement process is called registration. Registration is atomic, i.e. it either finishes successfully or it is aborted completely, but it will never destroy the contents of the old file during an abort. In the above example, the file is saved at the same location and with the same file type, but under another name.
ObxEx7 copies the contents of an old file to a newly created file. For that purpose, the old file produces a Files.Reader, and the new file produces a Files.Writer. These are the file riders mentioned earlier. They provide procedures to read, respectively to write, bytes, in the form of one-byte characters. Before they are used, their current positions must be set up (using their SetPos procedures). When a file reader attempts to read beyond the end of the file, it sets its eof flag to indicate this situation.
ObxEx7 shows another facility of the language Oberon/L: the ASSERT statement. An ASSERT statement checks a Boolean condition. If this condition is TRUE, it does nothing. If the condition is FALSE however, it aborts the program and gives an error message. The second parameter of an ASSERT, which must be an integer constant in the range 0..127, is displayed as part of the error message.
Oberon/F makes extensive use of ASSERT statements, in particular to make sure that its procedures are called with legal parameters. For example, the procedure Files.dir.Old asserts that its locator parameter is not NIL, otherwise error 20 occurs.
Apart from helping to detect errors early, assertions also are valuable as documentation of a program's properties. They are particularly helpful during design iterations and maintenance of a program.
Assertions which test for legal input state of a procedure are called precondition assertions. An assertion checking the outcome of a procedure is called a postcondition assertion. Assertions in the middle of a procedure, to check against errors in it, are called invariant assertions. Precondition assertions are the most important of these assertion types in Oberon/F. As a convention, all three types have their own assertion number ranges, as given below. The first range (0..19) is free for your use, typically as breakpoints using the HALT statement.
Free
0 ..
Preconditions
20 ..
Postconditions
60 ..
Invariants 100 .. 120
Reserved 121 .. 125
Not Yet Implemented 126
Interface Procedure Called 127
When an assertion error (or halt) occurs, a window is opened with the so-called debug trap text. Among other things, it displays the assertion number and the name of the procedure where this assertion was violated. You can use this information to look up what was wrong in the reference part of this book, or in the cross-reference section of the "Quick Start and Quick Reference" manual.
The following program is another example of how file riders and the end-of-file flag can be used. This example module could be used as a library module, i.e. a module which provides a generally useful service to other modules, but which is no complete application of its own:
WHILE ~a.eof & ~b.eof DO (* stop when one or both files have been read *)
IF inA < inB THEN (* choose smaller input *)
c.WriteByte(inA); a.ReadByte(inA)
ELSE
c.WriteByte(inB); b.ReadByte(inB)
END
END;
WHILE ~a.eof DO c.WriteByte(inA); a.ReadByte(inA) END;
WHILE ~b.eof DO c.WriteByte(inB); b.ReadByte(inB) END
END Merge;
END ObxEx8.
The next example illustrates how data can be written to a file, and how it can be retrieved from the file again. As with all typical applications, mappers are used instead of riders. A file mapper (either a reader or a writer) provides input/output routines for the basic Oberon/L data types and for character arrays (strings):
MODULE ObxEx9;
IMPORT Files, Stores;
PROCEDURE Write* (a: INTEGER; s: ARRAY OF CHAR; b: BOOLEAN);
VAR loc: Files.Locator; f: Files.File; w: Stores.Writer; res: LONGINT;
BEGIN
loc := Files.dir.This("");
f := Files.dir.New(loc);
w.ConnectTo(f); (* set up mapper w on file f *)
w.WriteString(s);
w.WriteInt(a);
w.WriteBool(b);
f.Register("Test2", "", res) (* should use a suitable file type here *)
END Write;
PROCEDURE Read* (VAR a: INTEGER; VAR s: ARRAY OF CHAR; VAR b: BOOLEAN);
VAR loc: Files.Locator; f: Files.File; r: Stores.Reader;
BEGIN
loc := Files.dir.This("");
f := Files.dir.Old(loc, "Test2", Files.shared); ASSERT(f # NIL, 20);
r.ConnectTo(f); (* set up mapper on file f *)
r.ReadString(s);
r.ReadInt(a);
r.ReadBool(b) (* a shareable file need not be closed, *)
END Read;
END ObxEx9.
In contrast to the earlier examples, the next program shows how a file is opened exclusively and updated locally, i.e. no new file is generated and registered, but an old one is modified in place:
MODULE ObxEx10;
IMPORT Files, Stores;
PROCEDURE Update*;
VAR loc: Files.Locator; f: Files.File; w: Stores.Writer;
BEGIN
loc := Files.dir.This("Obx");
loc := loc.This("Samples");
f := Files.dir.Old(loc, "Test0", Files.exclusive); (* access file Obx/Samples/Test0 *)
IF f # NIL THEN (* file is found and not yet open *)
w.ConnectTo(f);
w.SetPos(4);
w.WriteString("abcde"); (* overwrite five characters *)
f.Close (* exclusively opened files should be closed *)
END
END Update;
END ObxEx10.
Note that if the file were opened in shared mode, the w.ConnectTo(f) call would have been aborted with a run-time error: a writer cannot be connected to a file opened as shareable, because this implies a read-only permission.